name: CI

on:
  push:
    branches:
      - master
      - release-*
  pull_request: {}
  workflow_dispatch: {}

env:
  # Common versions
  GO_VERSION: '1.22.0'
  GOLANGCI_VERSION: 'v1.56.2'
  DOCKER_BUILDX_VERSION: 'v0.10.0'

  # Common users. We can't run a step 'if secrets.AWS_USR != ""' but we can run
  # a step 'if env.AWS_USR' != ""', so we copy these to succinctly test whether
  # credentials have been provided before trying to run steps that need them.
  DOCKER_USR: ${{ secrets.DOCKER_USR }}
  AWS_USR: ${{ secrets.AWS_USR }}
  UPBOUND_MARKETPLACE_PUSH_ROBOT_USR: ${{ secrets.UPBOUND_MARKETPLACE_PUSH_ROBOT_USR }}

jobs:
  check-diff:
    runs-on: ubuntu-22.04

    steps:
      - name: Checkout
        uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
        with:
          submodules: true

      - name: Setup Go
        uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5
        with:
          go-version: ${{ env.GO_VERSION }}

      - name: Find the Go Build Cache
        id: go
        run: echo "cache=$(make go.cachedir)" >> $GITHUB_OUTPUT

      - name: Cache the Go Build Cache
        uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 # v4
        with:
          path: ${{ steps.go.outputs.cache }}
          key: ${{ runner.os }}-build-check-diff-${{ hashFiles('**/go.sum') }}
          restore-keys: ${{ runner.os }}-build-check-diff-

      - name: Cache Go Dependencies
        uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 # v4
        with:
          path: .work/pkg
          key: ${{ runner.os }}-pkg-${{ hashFiles('**/go.sum') }}
          restore-keys: ${{ runner.os }}-pkg-

      - name: Vendor Dependencies
        run: make vendor vendor.check

      - name: Check Diff
        run: make check-diff

  detect-noop:
    runs-on: ubuntu-22.04
    outputs:
      noop: ${{ steps.noop.outputs.should_skip }}
    steps:
      - name: Detect No-op Changes
        id: noop
        uses: fkirc/skip-duplicate-actions@f75f66ce1886f00957d99748a42c724f4330bdcf # v5.3.1
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          paths_ignore: '["**.md", "**.png", "**.jpg"]'
          do_not_skip: '["workflow_dispatch", "schedule", "push"]'
          concurrent_skipping: false

  lint:
    runs-on: ubuntu-22.04
    needs: detect-noop
    if: needs.detect-noop.outputs.noop != 'true'

    steps:
      - name: Checkout
        uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
        with:
          submodules: true

      - name: Setup Go
        uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5
        with:
          go-version: ${{ env.GO_VERSION }}

      - name: Find the Go Build Cache
        id: go
        run: echo "cache=$(make go.cachedir)" >> $GITHUB_OUTPUT

      - name: Cache the Go Build Cache
        uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 # v4
        with:
          path: ${{ steps.go.outputs.cache }}
          key: ${{ runner.os }}-build-lint-${{ hashFiles('**/go.sum') }}
          restore-keys: ${{ runner.os }}-build-lint-

      - name: Cache Go Dependencies
        uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 # v4
        with:
          path: .work/pkg
          key: ${{ runner.os }}-pkg-${{ hashFiles('**/go.sum') }}
          restore-keys: ${{ runner.os }}-pkg-

      - name: Vendor Dependencies
        run: make vendor vendor.check

      # We could run 'make lint' to ensure our desired Go version, but we prefer
      # this action because it leaves 'annotations' (i.e. it comments on PRs to
      # point out linter violations).
      - name: Lint
        uses: golangci/golangci-lint-action@3a919529898de77ec3da873e3063ca4b10e7f5cc # v3
        with:
          version: ${{ env.GOLANGCI_VERSION }}
          skip-cache: true # We do our own caching.

  codeql:
    runs-on: ubuntu-22.04
    needs: detect-noop
    if: needs.detect-noop.outputs.noop != 'true'

    steps:
      - name: Checkout
        uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
        with:
          submodules: true

      - name: Setup Go
        uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5
        with:
          go-version: ${{ env.GO_VERSION }}

      - name: Find the Go Build Cache
        id: go
        run: echo "cache=$(make go.cachedir)" >> $GITHUB_OUTPUT

      - name: Cache the Go Build Cache
        uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 # v4
        with:
          path: ${{ steps.go.outputs.cache }}
          key: ${{ runner.os }}-build-check-diff-${{ hashFiles('**/go.sum') }}
          restore-keys: ${{ runner.os }}-build-check-diff-

      - name: Cache Go Dependencies
        uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 # v4
        with:
          path: .work/pkg
          key: ${{ runner.os }}-pkg-${{ hashFiles('**/go.sum') }}
          restore-keys: ${{ runner.os }}-pkg-

      - name: Vendor Dependencies
        run: make vendor vendor.check

      - name: Initialize CodeQL
        uses: github/codeql-action/init@e8893c57a1f3a2b659b6b55564fdfdbbd2982911 # v3
        with:
          languages: go

      - name: Perform CodeQL Analysis
        uses: github/codeql-action/analyze@e8893c57a1f3a2b659b6b55564fdfdbbd2982911 # v3

  trivy-scan-fs:
    runs-on: ubuntu-22.04
    needs: detect-noop
    if: needs.detect-noop.outputs.noop != 'true'
    steps:
      - name: Checkout
        uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
        with:
          submodules: true

      - name: Run Trivy vulnerability scanner in fs mode
        uses: aquasecurity/trivy-action@84384bd6e777ef152729993b8145ea352e9dd3ef # 0.17.0
        with:
          scan-type: 'fs'
          ignore-unfixed: true
          skip-dirs: design
          scan-ref: '.'
          exit-code: '1'
          severity: 'CRITICAL,HIGH'

  unit-tests:
    runs-on: ubuntu-22.04
    needs: detect-noop
    if: needs.detect-noop.outputs.noop != 'true'

    steps:
      - name: Checkout
        uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
        with:
          submodules: true

      - name: Fetch History
        run: git fetch --prune --unshallow

      - name: Setup Go
        uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5
        with:
          go-version: ${{ env.GO_VERSION }}

      - name: Find the Go Build Cache
        id: go
        run: echo "cache=$(make go.cachedir)" >> $GITHUB_OUTPUT

      - name: Cache the Go Build Cache
        uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 # v4
        with:
          path: ${{ steps.go.outputs.cache }}
          key: ${{ runner.os }}-build-unit-tests-${{ hashFiles('**/go.sum') }}
          restore-keys: ${{ runner.os }}-build-unit-tests-

      - name: Cache Go Dependencies
        uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 # v4
        with:
          path: .work/pkg
          key: ${{ runner.os }}-pkg-${{ hashFiles('**/go.sum') }}
          restore-keys: ${{ runner.os }}-pkg-

      - name: Vendor Dependencies
        run: make vendor vendor.check

      - name: Run Unit Tests
        run: make -j2 test

      - name: Publish Unit Test Coverage
        uses: codecov/codecov-action@ab904c41d6ece82784817410c45d8b8c02684457 # v3
        with:
          flags: unittests
          file: _output/tests/linux_amd64/coverage.txt

  e2e-tests:
    runs-on: ubuntu-22.04
    needs: detect-noop
    if: needs.detect-noop.outputs.noop != 'true'
    strategy:
      fail-fast: false
      matrix:
        test-suite:
          - base
          - environment-configs
          - usage
          - ssa-claims

    steps:
      - name: Setup QEMU
        uses: docker/setup-qemu-action@68827325e0b33c7199eb31dd4e31fbe9023e06e3 # v3
        with:
          platforms: all

      - name: Setup Docker Buildx
        uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3
        with:
          version: ${{ env.DOCKER_BUILDX_VERSION }}
          install: true

      - name: Checkout
        uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
        with:
          submodules: true

      - name: Fetch History
        run: git fetch --prune --unshallow

      - name: Setup Go
        uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5
        with:
          go-version: ${{ env.GO_VERSION }}

      - name: Find the Go Build Cache
        id: go
        run: echo "cache=$(make go.cachedir)" >> $GITHUB_OUTPUT

      - name: Cache the Go Build Cache
        uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 # v4
        with:
          path: ${{ steps.go.outputs.cache }}
          key: ${{ runner.os }}-build-e2e-tests-${{ hashFiles('**/go.sum') }}
          restore-keys: ${{ runner.os }}-build-e2e-tests-

      - name: Cache Go Dependencies
        uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 # v4
        with:
          path: .work/pkg
          key: ${{ runner.os }}-pkg-${{ hashFiles('**/go.sum') }}
          restore-keys: |
            ${{ runner.os }}-pkg-

      - name: Vendor Dependencies
        run: make vendor vendor.check

      - name: Build Helm Chart
        run: make -j2 build
        env:
          # We're using docker buildx, which doesn't actually load the images it
          # builds by default. Specifying --load does so.
          BUILD_ARGS: "--load"

      - name: Run E2E Tests
        run: make e2e E2E_TEST_FLAGS="-test.v -test.failfast -fail-fast --kind-logs-location ./logs-kind --test-suite ${{ matrix.test-suite }}"

      - name: Upload artifacts
        uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4
        if: failure()
        with:
          name: e2e-kind-logs-${{ matrix.test-suite }}
          path: ./logs-kind
          if-no-files-found: error
          retention-days: 7

  publish-artifacts:
    runs-on: ubuntu-22.04
    needs: detect-noop
    if: needs.detect-noop.outputs.noop != 'true'

    steps:
      - name: Cleanup Disk
        uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1
        with:
          android: true
          dotnet: true
          haskell: true
          tool-cache: true
          large-packages: false
          swap-storage: false

      - name: Setup QEMU
        uses: docker/setup-qemu-action@68827325e0b33c7199eb31dd4e31fbe9023e06e3 # v3
        with:
          platforms: all

      - name: Setup Docker Buildx
        uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3
        with:
          version: ${{ env.DOCKER_BUILDX_VERSION }}
          install: true

      - name: Checkout
        uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
        with:
          submodules: true

      - name: Fetch History
        run: git fetch --prune --unshallow

      - name: Setup Go
        uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5
        with:
          go-version: ${{ env.GO_VERSION }}

      - name: Find the Go Build Cache
        id: go
        run: echo "cache=$(make go.cachedir)" >> $GITHUB_OUTPUT

      - name: Cache the Go Build Cache
        uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 # v4
        with:
          path: ${{ steps.go.outputs.cache }}
          key: ${{ runner.os }}-build-publish-artifacts-${{ hashFiles('**/go.sum') }}
          restore-keys: ${{ runner.os }}-build-publish-artifacts-

      - name: Cache Go Dependencies
        uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 # v4
        with:
          path: .work/pkg
          key: ${{ runner.os }}-pkg-${{ hashFiles('**/go.sum') }}
          restore-keys: ${{ runner.os }}-pkg-

      - name: Vendor Dependencies
        run: make vendor vendor.check

      - name: Build Artifacts
        run: make -j2 build.all
        env:
          # We're using docker buildx, which doesn't actually load the images it
          # builds by default. Specifying --load does so.
          BUILD_ARGS: "--load"

      - name: Publish Artifacts to GitHub
        uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4
        with:
          name: output
          path: _output/**

      - name: Login to DockerHub
        uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3
        if: env.DOCKER_USR != ''
        with:
          username: ${{ secrets.DOCKER_USR }}
          password: ${{ secrets.DOCKER_PSW }}

      - name: Login to Upbound
        uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3
        if: env.UPBOUND_MARKETPLACE_PUSH_ROBOT_USR != ''
        with:
          registry: xpkg.upbound.io
          username: ${{ secrets.UPBOUND_MARKETPLACE_PUSH_ROBOT_USR }}
          password: ${{ secrets.UPBOUND_MARKETPLACE_PUSH_ROBOT_PSW }}

      - name: Publish Artifacts to S3, Marketplace, DockerHub
        run: make -j2 publish BRANCH_NAME=${GITHUB_REF##*/}
        if: env.AWS_USR != '' && env.DOCKER_USR != '' && env.UPBOUND_MARKETPLACE_PUSH_ROBOT_USR != ''
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_USR }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_PSW }}
          AWS_DEFAULT_REGION: us-east-1
          GIT_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          DOCS_GIT_USR: ${{ secrets.UPBOUND_BOT_GITHUB_USR }}
          DOCS_GIT_PSW: ${{ secrets.UPBOUND_BOT_GITHUB_PSW }}

      - name: Promote Artifacts in S3, DockerHub
        if: github.ref == 'refs/heads/master' && env.AWS_USR != '' && env.DOCKER_USR != '' && env.UPBOUND_MARKETPLACE_PUSH_ROBOT_USR != ''
        run: make -j2 promote
        env:
          BRANCH_NAME: master
          CHANNEL: master
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_USR }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_PSW }}
          AWS_DEFAULT_REGION: us-east-1

  fuzz-test:
    runs-on: ubuntu-22.04
    needs: detect-noop
    if: needs.detect-noop.outputs.noop != 'true'

    steps:
      # TODO(negz): Can we make this use our Go build and dependency cache? It
      # seems to build Crossplane inside of a Docker image.
      - name: Build Fuzzers
        id: build
        uses: google/oss-fuzz/infra/cifuzz/actions/build_fuzzers@master
        with:
          oss-fuzz-project-name: "crossplane"
          language: go

      - name: Run Fuzzers
        uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@master
        with:
          oss-fuzz-project-name: "crossplane"
          fuzz-seconds: 300
          language: go

      - name: Upload Crash
        uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4
        if: failure() && steps.build.outcome == 'success'
        with:
          name: artifacts
          path: ./out/artifacts

  # TODO(negz): Refactor this job. Should the parts pertaining to release
  # branches live in promote.yaml instead?
  protobuf-schemas:
    runs-on: ubuntu-22.04
    needs: detect-noop
    if: needs.detect-noop.outputs.noop != 'true'

    steps:
      - name: Checkout
        uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4

      - name: Setup Buf
        uses: bufbuild/buf-setup-action@v1
      
      - name: Lint Protocol Buffers
        uses: bufbuild/buf-lint-action@v1
        with:
          input: apis

      - name: Detect Breaking Changes in Protocol Buffers (Master Branch)
        uses: bufbuild/buf-breaking-action@a074e988ee34efcd4927079e79c611f428354c01 # v1
        # We want to run this for the master branch, and PRs.
        if: ${{ ! startsWith(github.ref, 'refs/heads/release-') }}
        with:
          input: apis
          against: "https://github.com/${GITHUB_REPOSITORY}.git#branch=master,subdir=apis"

      - name: Detect Breaking Changes in Protocol Buffers (Release Branch)
        uses: bufbuild/buf-breaking-action@a074e988ee34efcd4927079e79c611f428354c01 # v1
        # We want to run this only on release branches.
        if: ${{ startsWith(github.ref, 'refs/heads/release-') }}
        with:
          input: apis
          against: "https://github.com/${GITHUB_REPOSITORY}.git#branch=${GITHUB_REF_NAME},subdir=apis"
        
      - name: Push Protocol Buffers to Buf Schema Registry
        if: ${{ github.repository == 'crossplane/crossplane' && (github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/heads/release-')) }}
        uses: bufbuild/buf-push-action@v1
        with:
          input: apis
          buf_token: ${{ secrets.BUF_TOKEN }}
